import { customRef, reactive, ref, watch, WatchOptions } from "vue";

export interface UseComputedAsyncOption<
  D extends object = object,
  T = unknown
> {
  watcher: () => T;
  getter: (watcherNewValue?: T, watcherOldValue?: T) => D;
  initial: () => D | D;
  watchOption: WatchOptions;
}

/**
 * @param {UseComputedAsyncOption} option
 */
export function useComputedAsync<D extends object = object, T>({
  watcher,
  getter,
  initial,
  watchOption
}: UseComputedAsyncOption<D, T>) {
  function _initial() {
    return typeof initial === "function" ? initial() : initial;
  }

  let _result = reactive(_initial());
  let _trigger: () => void;
  let _dirty = false;
  let _watcherResult: [watcherNewValue?: T, watcherOldValue?: T] = [];

  function _setResult(value: D) {
    _result = reactive(value);
    _trigger();
  }

  const loading = ref(false);

  const data = customRef((refTrack, refTrigger) => {
    _trigger = refTrigger;

    return {
      set() {},
      get() {
        _dirty && trigger();
        refTrack();
        return _result;
      }
    };
  });

  async function trigger() {
    _dirty = false;
    loading.value = true;

    try {
      const getterValue = await getter(..._watcherResult);
      _setResult(getterValue);
    } catch (error) {
      reset();
      if (import.meta.env.DEV) {
        throw error;
      }
    } finally {
      loading.value = false;
      if (_dirty) trigger();
    }
  }

  function reset(...params: [D] | []) {
    _setResult(params.length ? params[0] : _initial());
  }

  let { immediate } = watchOption ?? {};

  const stop = watcher
    ? watch(
        watcher,
        (...params) => {
          _watcherResult = params as any;
          _dirty = true;

          if (immediate) {
            immediate = false;
            trigger();
          }
        },
        watchOption
      )
    : (trigger(), () => {});

  return {
    data,
    loading,
    trigger,
    reset,
    stop,
    *[Symbol.iterator]() {
      yield data;
      yield loading;
      yield trigger;
      yield reset;
      yield stop;
    }
  };
}
